#!/usr/bin/env python

#
# sfn.py
#
# Copyright 2007 Helsinki Institute for Information Technology
# and the authors.
#
# Authors: Toni Ruottu <toni.ruottu@iki.fi>
#

# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

import sets
import sys
import os
import re
from fusion.db.db import DB

class Filesfile:

    def __init__(self, path):
        self.path = path + ('.sfn', 'files')

    def read(self):

        filename = os.path.join(*self.path)

        names = []
        try:
            file = open(filename, 'r')
            names += file.read().split('\n')
            file.close()
        except:
            pass

        return names

    def write(self, names):

        dirpath = list(self.path)
        dirpath.pop()
        dirpathname = os.path.join(*dirpath)

        try:
            os.makedirs(dirpathname)
        except OSError:
            pass

        filename = os.path.join(*self.path)

        file = open ( filename, 'w' )
        file.write(('\n'.join(sets.Set(names))))
        file.close()

class Targetfile:

    def __init__(self, path):

        pathname = os.path.join(*path)

        if not os.path.isdir(pathname):
            path = list(path)
            path.pop()
        self.path = tuple(path) + ('.sfn', 'target')

    def read(self):

        
        pathname = os.path.join(*self.path)
        file = open (pathname, 'r' )
        return file.read().split('\n').pop(0)

    def write(self, url):

        dirpath = list(self.path)
        dirpath.pop()
        dirpathname = os.path.join(*dirpath)

        try:
            os.makedirs(dirpathname)
        except OSError:
            pass

        pathname = os.path.join(*self.path)
        file = open ( pathname, 'w' ) 
        file.write(url.encode('utf8'))
        file.close()

class Plugin:

    commands = []

    def crush_pathnames(self, paths):
        p = []
        for path in paths:
            path = os.path.normpath(path)

            crushed = []
            oldpath = ''

            while path != oldpath:
                oldpath = path
                path, piece = os.path.split(path)
                if piece:
                    crushed.append(piece)
            if path:
                crushed.append(path)
            crushed.reverse()
            p.append(tuple(crushed))
        return sets.Set(p)

    def expand_paths(self, paths):
        p=[]

        for path in paths:
            if not '.sfn' in path:
                pathname = os.path.join(*path)
                if os.path.isdir(pathname):
                    files = [os.path.join(pathname, i) for i in os.listdir(pathname)]
                    p += self.expand_paths(self.crush_pathnames(files))
                else:
                    pathname = os.path.join(*path)
                    pathname = os.path.normpath(pathname)
                    p += self.crush_pathnames((pathname,))
        return sets.Set(p)

    def expand_dirs(self, paths):
        p=[]

        for path in paths:
            if not '.sfn' in path:
                pathname = os.path.join(*path)
                if os.path.isdir(pathname):
                    files = [os.path.join(pathname, i) for i in os.listdir(pathname)]
                    p += self.expand_dirs(self.crush_pathnames(files))

                    pathname = os.path.join(*path)
                    pathname = os.path.normpath(pathname)
                    p += self.crush_pathnames((pathname,))
        return sets.Set(p)

    def selected_files(self, paths):
        p=[]

        for dir in self.expand_dirs(paths):

            ff = Filesfile(dir)

            for filename in ff.read():
                if filename:
                    path = dir + (filename,)

                    pathname = os.path.join(*path)
                    pathname = os.path.normpath(pathname)

                    p += self.crush_pathnames((pathname,))

        return sets.Set(p)

    def createsfntree(self, rootpath, subpaths, url):

        subpaths = sets.Set(subpaths) | sets.Set([()])

        for subpath in subpaths:
            path = rootpath + subpath
            try:
                pathname = os.path.join(*path)
                os.makedirs(pathname)
            except OSError:
                pass
    
            fullurl = url

            if subpath:
                fullurl = url + os.path.join(*subpath)

            tf = Targetfile(path)
            tf.write(fullurl)

    def parsecollectionurl(self, url):

        match = re.search('fusion:///collections?/([0-9a-f]{40})/(.*)', url)
        if match:
            col, dir = match.groups()
            if dir and dir[-1] == '/':
                dir = dir[:-1]
            return (col, dir.split('/'))

        match = re.search('http://[^/]*/collections?/([0-9a-f]{40})/(.*)', url)
        if match:

            col, dir = match.groups()
            guess = 'fusion:///collection/' + col + '/' + dir
            raise ValueError('http', guess)

        print "invalid url: " + url
        sys.exit()

    def getdirectories(self, keys):
        fullpaths = []
        paths = [k.split('/') for k in keys]

        for path in paths:
            path.pop()

            while path:
                fullpaths.append(tuple(path))
                path.pop()

        return sets.Set(fullpaths)

    def fusionjoin(self, a, *p):
        """
        Ripped from Python 2.5 posixpath module :-)
        """
        path = a
        for b in p:
            if b.startswith('/'):
                path = b
            elif path == '' or path.endswith('/'):
                path +=  b
            else:
                path += '/' + b

        return path

class Hello(Plugin):

    def __init__(self):
        self.commands = (None, 'hello')

    def execute(self, args):
        print 'Hello world'

class Select(Plugin):

    def __init__(self):
        self.commands = ('select',)

    def execute(self, args):

        pathnames = args[2:]
        pathnames = [os.path.normpath(p) for p in pathnames]
        paths = self.crush_pathnames(pathnames)

        for path in self.expand_paths(paths):

            print os.path.join(*path)

            path = list(path)
            file = path.pop()
            path = tuple(path)

            ff = Filesfile(path)
            selected = ff.read()
            selected.append(file)
            ff.write(selected)

class Deselect(Plugin):

    def __init__(self):
        self.commands = ('deselect',)

    def execute(self, args):

        pathnames = args[2:]
        pathnames = [os.path.normpath(p) for p in pathnames]
        paths = self.crush_pathnames(pathnames)

        for path in self.expand_paths(paths):

            print os.path.join(*path)

            path = list(path)
            file = path.pop()
            path = tuple(path)

            ff = Filesfile(path)
            selected = ff.read()

            while file in selected:
                selected.remove(file)

            ff.write(selected)
       
class Status (Plugin):

    def __init__(self):
        self.commands = ('status',)

    def execute(self, args):
        
        pathnames = args[2:] or ['.']
        pathnames = [os.path.normpath(p) for p in pathnames]
        paths = self.crush_pathnames(pathnames)
        
        existing = self.expand_paths(paths)
        selected = self.selected_files(paths)

        symbols = {}

        symbols['!'] = sets.Set(selected - existing)
        symbols['s'] = sets.Set(selected & existing)
        symbols[''] = sets.Set(existing - selected)


        files = {}
        for s in symbols.keys():
            for f in symbols[s]:
                files[f] = s

        keys=files.keys()
        keys.sort()

        statuses = []

        for file in keys:
            statuses.append(files[file].ljust(2) + str(os.path.join(*file)))

        print '\n'.join(statuses)

class Unittest(Plugin):

    def __init__(self):
        self.commands = (None, 'unit')

    def execute(self, args):
        import doctest
        doctest.testmod()

class Help(Plugin):

    def __init__(self, plugins):
        self.commands = ('help', 'h')
        self.plugins = plugins

    def execute(self, args):

        version = '0.1.0'
        name = args[0]

        helps = [
            'SubFusioN ' + version,
            '',
            'Commands:',
        ]
        print '\n'.join(helps)

        commands = [ c.commands[0] for c in self.plugins ]
        commands.sort()
        print '\n'.join( [4*' '+c for c in commands if c] )

class Info(Plugin):

    def __init__(self):
        self.commands = ('info',)

    def execute(self, args):

        pathnames = args[2:] or ['.']
        pathnames = [os.path.normpath(p) for p in pathnames]
        dirs = self.crush_pathnames(pathnames)

        for dir in dirs:
            tf = Targetfile(dir)
            print str(os.path.join(*dir)) + ' - ' + tf.read()

class List(Plugin):

    def __init__(self):
        self.commands = ('list',)

    def execute(self, _):

        cols = {}

        db = DB('~/.fusion/fusion.db')
        for col in db.list_collections():
            title = db.get_text(col, 'info/title') or ''
            cols[str(title.encode('utf8'))] = 'fusion:///collection/' + col + '/'
        db.close()

        keys = cols.keys()
        keys.sort()

        for key in keys:
            print cols[key] + ' (' + key.split('\n')[0] + ')'



class Init(Plugin):

    def __init__(self):
        self.commands = ('init',)

    def execute(self, args):

        pathnames = args[2:]
        pathnames = [os.path.normpath(p) for p in pathnames]
        paths = self.crush_pathnames(pathnames)

        for path in paths:

            db = DB('~/.fusion/fusion.db')
            id = db.create_collection()
            db.close()

            url = 'fusion:///collection/' + id + '/'

            dirs = [i[len(path):] for i in self.expand_dirs([path])]

            self.createsfntree(path, dirs, url)

class Checkout(Plugin):

    def __init__(self):
        self.commands = ('checkout',)

    def execute(self, args):

        url = args[2]

        try:
            col, dir = self.parsecollectionurl(url)
        except ValueError, ve:

            type, guess = ve

            if type == 'http':
                args[2]=guess

                print 'SubFusioN is not able to handle http. Try executing...'
                print
                print ' '.join(args)
                print
                print 'It just might do the trick.'

            sys.exit()

        print '<- ' + url

        db = DB('~/.fusion/fusion.db')

        local = (dir[-1],)

        if not local[0]:

            title = db.get_text(col, 'info/title')
            local = (title.split('\n').pop(0),)
        keys = db.keys(col, self.fusionjoin(*dir))

        dirs = self.getdirectories(keys)

        self.createsfntree(local, dirs, url)
        
        skips = []

        for subpath in [p.split('/') for p in keys]:

            key = self.fusionjoin(*(dir + subpath))

            if key:

                value = db.get(col, key)

                if tuple(subpath) in dirs:
                    skips.append(' (!) Skipped fusion:///collection/' + col +  key + "' (collection includes a directory with the same name)")
                else:
                    filepath = os.path.join(*(local + tuple(subpath) ))
                    print filepath
                    file = open(filepath, 'w')
                    file.write(value)
                    file.close()
        db.close()

        print '\n'.join(skips)

class Commit(Plugin):

    def __init__(self):
        self.commands = ('commit',)

    def execute(self, args):

        pathnames = args[2:] or ['.']
        pathnames = [os.path.normpath(p) for p in pathnames]
        paths = self.crush_pathnames(pathnames)

        existing = self.expand_paths(paths)
        selected = self.selected_files(paths)

        for file in selected & existing:

            tf = Targetfile(file)
            url = tf.read()
            col, dir = self.parsecollectionurl(url)
            
            filename = os.path.join(*file)
            content = open(filename).read()

            key = [file[-1]]

            if dir != ['']:
                key = dir + key

            print '-> fusion:///collection/' + col + '/' + self.fusionjoin(*key)

            db = DB('~/.fusion/fusion.db')
            v = db.put(col, self.fusionjoin(*key), content)
            db.close()

        for file in selected - existing:
            print 'File ' + file + " is missing. (You may use 'remove' command to deselect it.)"

if __name__ == "__main__":

    plugins = [Hello(), Status(), Info(), Select(), Deselect(), Init(), Unittest(), Checkout(), Commit(), List()]

    help = Help(plugins)
    plugins.append(help)
    h = help.commands[0]

    commands = {}

    for plugin in plugins:
        for command in plugin.commands:
            commands[command] = plugin

    command = h

    if len(sys.argv) > 1:
        command = sys.argv[1]

    if not command in commands.keys():
        print "Unknown command: '" + command + "'"
        command = h

    commands[command].execute(sys.argv)
